package edu.gmu.projects.fireresponder.fusionengine;

import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import edu.gmu.projects.fireresponder.comm.MessageListener;
import edu.gmu.projects.fireresponder.environment.Asset;
import edu.gmu.projects.fireresponder.environment.Location;
import edu.gmu.projects.fireresponder.missionplanner.SituationEnvironment;
import edu.gmu.projects.fireresponder.xml.alerts.Alert;
import edu.gmu.projects.util.geo.CoordinateCalculator;

public class FusionEngine implements MessageListener, IFusionInterface {

	private Map<Location, AnchorPoint> fireAnchorPoints;
	private Map<Location, AnchorPoint> presenceAnchorPoints;
	private List<FusionEngineListener> listeners;

	private SituationEnvironment environment;
	private Thread presenceDecayThread;
	private PresenceDecayRunnable presenceDecay;

	// distance (in km) that is considered for an anchor to be relevant. This
	// provides the weight to consider an alert, if at all
	// Alerts more than 50 meters have no value...
	public static final double MAX_DISTANCE_FOR_RELEVANT_FIRE_ANCHOR = .05;
	public static final double MAX_DISTANCE_FOR_RELEVANT_PRESENCE_ANCHOR = .02;

	// growth rate of fire in km/h
	public static final double GROWTH_RATE_OF_FIRE = 5;

	// number of points to check for a fire in to predict fire spreading to a
	// given location
	public static final int POINTS_IN_CIRCLE = 8;

	public static enum ProbabilityCategory {
		DEFINITELY_NOT, HIGHLY_IMPROBABLE, IMPROBABLE, POSSIBLE, PROBABLE, HIGHLY_PROBABLE, DEFINITE
	};

	public static ProbabilityCategory getProbabilityCategory(double val) {
		if (val > .99) {
			return ProbabilityCategory.DEFINITE;
		}
		if (val > .8) {
			return ProbabilityCategory.HIGHLY_PROBABLE;
		}
		if (val > .6) {
			return ProbabilityCategory.PROBABLE;
		}
		if (val > .4) {
			return ProbabilityCategory.POSSIBLE;
		}
		if (val > .2) {
			return ProbabilityCategory.IMPROBABLE;
		}
		if (val > .01) {
			return ProbabilityCategory.HIGHLY_IMPROBABLE;
		}
		return ProbabilityCategory.DEFINITELY_NOT;
	}

	public static String getProbabilityCategoryText(double value) {
		return getProbabilityCategoryText(getProbabilityCategory(value));
	}

	public static String getProbabilityCategoryText(ProbabilityCategory category) {
		switch (category) {
		case DEFINITE:
			return "DEFINITE";
		case HIGHLY_PROBABLE:
			return "HIGHLY PROBABLE";
		case PROBABLE:
			return "PROBABLE";
		case IMPROBABLE:
			return "IMPROBABLE";
		case HIGHLY_IMPROBABLE:
			return "HIGHLY IMPROBABLE";
		}
		
		return "DEFINITELY NOT";
	}
	
	public FusionEngine() {
		init();
		loadSituationEnvironment();
	}

	public FusionEngine(InputStream buildings, InputStream paths,
			InputStream safeSpots) {
		init();
		loadSituationEnvironment(buildings, paths, safeSpots);
	}

	private void init() {
		fireAnchorPoints = Collections
				.synchronizedMap(new HashMap<Location, AnchorPoint>());
		presenceAnchorPoints = Collections
				.synchronizedMap(new HashMap<Location, AnchorPoint>());
		listeners = Collections
				.synchronizedList(new LinkedList<FusionEngineListener>());
		
		
		presenceDecay = new PresenceDecayRunnable(this);
		presenceDecayThread = new Thread(presenceDecay, "Presence Decay Thread");
		presenceDecayThread.start();
	}

	private void loadSituationEnvironment() {
		try {
			InputStream buildings = new FileInputStream(
					"json/building-data.json");
			InputStream paths = new FileInputStream("json/path-data.json");
			InputStream safeSpots = new FileInputStream(
					"json/safespot-data.json");
			loadSituationEnvironment(buildings, paths, safeSpots);
		} catch (Exception e) {
			environment = null;
		}
	}

	private void loadSituationEnvironment(InputStream buildings,
			InputStream paths, InputStream safeSpots) {
		environment = SituationEnvironment.getSituationEnvironment(buildings,
				paths, safeSpots);
	}

	public double getFireProbability(Location location) {
		double probability = 0;
		Location[] locations = (Location[]) fireAnchorPoints.keySet().toArray(
				new Location[0]);
		for (Location l : locations) {
			double distance = CoordinateCalculator.getDistance(location, l);
			if (distance < MAX_DISTANCE_FOR_RELEVANT_FIRE_ANCHOR) {
				double weight = 1 - (distance / MAX_DISTANCE_FOR_RELEVANT_FIRE_ANCHOR);
				probability += weight
						* fireAnchorPoints.get(l).getPointProbability();
			}
		}

		if (probability > 1) {
			probability = 1;
		}
		return probability;
	}

	public double getFireProbability(double latitude, double longitude) {
		Location l = new Location();
		l.setLatitude(latitude);
		l.setLongitude(longitude);
		return getFireProbability(l);
	}

	public double getFireProbability(double latitude, double longitude,
			int timeInMillis) {
		Location l = new Location();
		l.setLatitude(latitude);
		l.setLongitude(longitude);
		return getFireProbability(l, timeInMillis);
	}

	public double getFireProbability(Location location, int timeInMillis) {
		if (timeInMillis == 0) {
			return getFireProbability(location);
		}

		final double distance = GROWTH_RATE_OF_FIRE * timeInMillis / 3600000d;

		// find a set of evenly spaced points on the circle that surrounds this
		// center point, with
		// a radius that of the distance we expect the fire to travel in
		// 'timeInMillis' milliseconds
		//
		// Probability of a fire here is the maximum probability of a fire at
		// any of those spots now

		double maxProbability = getFireProbability(location);
		for (int i = 0; i < POINTS_IN_CIRCLE; i++) {
			Location p = CoordinateCalculator.getPoint(location, distance, i
					* 2 * Math.PI / POINTS_IN_CIRCLE);
			double edgeProb = getFireProbability(p);
			if (edgeProb > maxProbability) {
				maxProbability = edgeProb;
			}
		}

		return maxProbability;
	}

	public double getPresenceProbability(Location location) {
		double probability = 0;
		Location[] locations = (Location[]) presenceAnchorPoints.keySet()
				.toArray(new Location[0]);
		for (Location l : locations) {
			double distance = CoordinateCalculator.getDistance(location, l);
			if (distance < MAX_DISTANCE_FOR_RELEVANT_PRESENCE_ANCHOR) {
				double weight = 1 - (distance / MAX_DISTANCE_FOR_RELEVANT_PRESENCE_ANCHOR);
				probability += weight
						* presenceAnchorPoints.get(l).getPointProbability();
			}
		}

		if (probability > 1) {
			probability = 1;
		}
		return probability;
	}

	public double getFireDistance(double latitude, double longitude,
			double minProb) {
		Location l = new Location();
		l.setLatitude(latitude);
		l.setLongitude(longitude);

		return getFireDistance(l, minProb);
	}

	public double getFireDistance(Asset asset, double minProb) {
		double distance = 0;
		boolean initial = true;

		for (Location l : asset.getPoints()) {
			if (initial == true) {
				distance = getFireDistance(l, minProb);
				initial = false;
			} else {
				distance = Math.min(distance, getFireDistance(l, minProb));
			}
		}

		return distance;
	}

	public double getFireDistance(Location location, double minProb) {
		final double RADIUS_CHECK_MULTIPLIER = 2;
		double distance = .01;
		double oldDistance = 0;

		double[] oldprob = new double[POINTS_IN_CIRCLE];
		double[] newprob = new double[POINTS_IN_CIRCLE];
		double maxFound = 0;
		int directionFound = -1;

		double currentProb = getFireProbability(location);
		if (currentProb > minProb) {
			return 0;
		}
		for (int i = 0; i < POINTS_IN_CIRCLE; i++) {
			newprob[i] = currentProb;
		}

		do {

			for (int i = 0; i < POINTS_IN_CIRCLE; i++) {
				oldprob[i] = newprob[i];
				Location p = CoordinateCalculator.getPoint(location, distance,
						i * 2 * Math.PI / POINTS_IN_CIRCLE);
				double edgeProb = getFireProbability(p);
				newprob[i] = edgeProb;
				if (edgeProb > maxFound) { // we found a new best
					maxFound = edgeProb;
					if (maxFound >= minProb) { // our new best is good enough to
												// satisfy our requirement
						directionFound = i;
					}
				}
			}
			if (directionFound == -1) {
				oldDistance = distance;
				distance *= RADIUS_CHECK_MULTIPLIER;
			}
		} while (directionFound == -1 && distance < 25); // look up to 25 km
															// away

		if (directionFound == -1) {
			return Double.MAX_VALUE;
		} else {
			double deltaBetweenPoints = newprob[directionFound]
					- oldprob[directionFound];
			double overWeight = 1 - ((newprob[directionFound] - minProb) / deltaBetweenPoints);
			double underWeight = 1 - ((minProb - oldprob[directionFound]) / deltaBetweenPoints);

			return (overWeight * distance) + (underWeight * oldDistance);
		}
	}

	public double getPresenceProbability(double latitude, double longitude) {
		Location l = new Location();
		l.setLatitude(latitude);
		l.setLongitude(longitude);
		return getPresenceProbability(l);
	}

	public double getPresenceProbability(Location location, int timeInMillis) {
		// i am going to guess people don't move
		return getPresenceProbability(location);
	}

	public double getPresenceProbability(double latitude, double longitude,
			int timeInMillis) {
		Location l = new Location();
		l.setLatitude(latitude);
		l.setLongitude(longitude);
		return getPresenceProbability(l, timeInMillis);
	}

	public void receivedAlertMessage(Alert alert) {
		// System.out.println("#########3 fusions engine:" + alert.toString());
		alert.getTime().getTimeInMillis();

		if (FireAnchorPoint.isValidAlert(alert)) {
			AnchorPoint ap = getFireAnchorPoint(alert);
			ap.addAlert(alert);
			updateFireProbabilityForAssets(ap);
		}

		if (PresenceAnchorPoint.isValidAlert(alert)) {
			AnchorPoint ap = getPresenceAnchorPoint(alert);
			ap.addAlert(alert);
			updatePresenceProbabilityForAssets(ap);
		}
	}

	private AnchorPoint getFireAnchorPoint(Alert alert) {
		AnchorPoint ap;
		if (fireAnchorPoints.containsKey(alert.getLocation())) {
			ap = fireAnchorPoints.get(alert.getLocation());
		} else {
			ap = createAnchorPoint(alert.getLocation(), new FireAnchorPoint());
			fireAnchorPoints.put(alert.getLocation(), ap);
		}
		return ap;
	}

	private AnchorPoint createAnchorPoint(Location location, AnchorPoint ap) {
		ap.setLatitude(location.getLatitude());
		ap.setLongitude(location.getLongitude());

		List<Asset> nearByAssets = new LinkedList<Asset>();
		for (Asset asset : environment.getAssetSet()) {
			for (Location point : asset.getPoints()) {
				if (CoordinateCalculator.getDistance(point, location) < MAX_DISTANCE_FOR_RELEVANT_FIRE_ANCHOR) {
					nearByAssets.add(asset);
					break;
				}
			}
		}

		ap.setNearByAssets(nearByAssets);
		return ap;
	}

	protected void updateFireProbabilityForAssets(AnchorPoint point) {
		for (Asset asset : point.getNearByAssets()) {
			double originalProb = asset.getProbabilityOfFire();
			double newProb = originalProb;
			double pointProb = 0;
			for (Location loc : asset.getPoints()) {
				pointProb = getFireProbability(loc);
				if (pointProb > newProb) {
					newProb = pointProb;
				}
			}
			if (newProb > originalProb) {
				asset.setProbabilityOfFire(newProb);
				if (getProbabilityCategory(originalProb) != getProbabilityCategory(newProb)) {
					informListenersOfFire(asset);
				}
			}
		}
	}

	protected void updatePresenceProbabilityForAssets(AnchorPoint point) {
		for (Asset asset : point.getNearByAssets()) {
			double originalProb = asset.getProbabilityOfPresence();
			double newProb = 0;
			double pointProb = 0;
			for (Location loc : asset.getPoints()) {
				pointProb = getPresenceProbability(loc);
				if (pointProb > newProb) {
					newProb = pointProb;
				}
			}
			asset.setProbabilityOfPresence(newProb);
			if (getProbabilityCategory(originalProb) != getProbabilityCategory(newProb)) {
				informListenersOfPresence(asset);
			}
		}
	}

	private AnchorPoint getPresenceAnchorPoint(Alert alert) {
		AnchorPoint ap;
		if (presenceAnchorPoints.containsKey(alert.getLocation())) {
			ap = presenceAnchorPoints.get(alert.getLocation());
		} else {
			ap = createAnchorPoint(alert.getLocation(),
					new PresenceAnchorPoint());
			presenceAnchorPoints.put(alert.getLocation(), ap);
		}
		presenceDecay.addPresenseAlert((PresenceAnchorPoint) ap);
		return ap;
	}

	private void informListenersOfFire(Asset asset) {
		for (FusionEngineListener listener : listeners) {
			listener.fireProbabilityUpdated(asset);
		}
	}

	private void informListenersOfPresence(Asset asset) {
		for (FusionEngineListener listener : listeners) {
			listener.presenceProbabilityUpdated(asset);
		}
	}

	public void addListener(FusionEngineListener listener) {
		listeners.add(listener);
	}

	public void removeListener(FusionEngineListener listener) {
		if (listeners.contains(listener)) {
			listeners.remove(listener);
		}
	}

	public double getFireProbability(Asset asset) {
		return asset.getProbabilityOfFire();
	}

	public double getFireProbability(Asset asset, int timeInMillis) {
		double prob = 0;
		double pointProb = 0;
		for (Location loc : asset.getPoints()) {
			pointProb = getFireProbability(loc, timeInMillis);
			if (pointProb > prob) {
				prob = pointProb;
			}
		}

		return prob;
	}

	public double getPercentageOnFire(double minCertainty, Asset asset) {
		return getPercentageOnFire(minCertainty, asset, 0);
	}

	// this is called percentage, implying the 42% kind of answer, but it
	// provides the .42 kind of answer... is this a problem?
	public double getPercentageOnFire(double minCertainty, Asset asset,
			int timeInMillis) {
		// used doubles to force floating point divide for return value
		double positionsOnFire = 0;
		double totalPositions = asset.getPoints().size();
		for (Location loc : asset.getPoints()) {
			double pointProb = getFireProbability(loc, timeInMillis);
			if (pointProb >= minCertainty) {
				positionsOnFire += 1;
			}
		}

		return (positionsOnFire / totalPositions);
	}

	public double getPresenceProbability(Asset asset) {
		return asset.getProbabilityOfPresence();
	}

	public double getPresenceProbability(Asset asset, int timeInMillis) {
		return asset.getProbabilityOfPresence();
	}

}
